from contextlib import contextmanager
import gtk
import goocanvas
import pango
import cairo
import pygraphviz
import pydot
from collections import defaultdict
from functools import partial

class BaseApp(object):
    """Base clickonpy GUI application.
    Create a window with a scrollable goocanvas and zooming capabilities
    """
    DEFAULT_SIZE = (900, 700)
    
    def __init__(self):
        with self.mk_main_window() as window:
            with self.mk_main_vbox(window) as vbox:
                with self.mk_hscale(vbox):
                    pass
                with self.mk_scrolled_window(vbox) as scrolled_window:
                    with self.mk_canvas(scrolled_window):
                        pass

    @contextmanager
    def mk_main_window(self):
        self.main_window = window = gtk.Window()
        window.set_default_size(*self.DEFAULT_SIZE)
        window.connect("delete-event", gtk.main_quit)
        yield window
        window.show_all()
        
    @contextmanager
    def mk_main_vbox(self, window):
        self.main_vbox = vbox = gtk.VBox(False, 10)
        window.add(vbox)
        vbox.show()
        yield vbox
        
    @contextmanager
    def mk_hscale(self, vbox):
        m, M = 0.2, 4.0
        diff = M - m
        adj = gtk.Adjustment(1.0, m, M, diff/1000, diff/10, diff/10)
        adj.connect("value_changed", self.zoom_changed)
        self.hscale = hscale = gtk.HScale(adj)
        set_scale_default_values(hscale)
        vbox.pack_start(hscale, False, False, 0)
        hscale.show()
        yield hscale
        
    @contextmanager
    def mk_scrolled_window(self, vbox):
        self.scrolled_window = scrolled_window = gtk.ScrolledWindow()
        scrolled_window.set_policy(gtk.POLICY_ALWAYS, gtk.POLICY_ALWAYS)
        vbox.pack_start(scrolled_window)
        yield scrolled_window
        
    @contextmanager
    def mk_canvas(self, window):
        self.canvas = canvas =  goocanvas.Canvas()
        window.add(canvas)
        yield canvas
        
        
    def main(self):
        gtk.main()

    
    def zoom_changed(self, adjustment):
        pass
    
def set_scale_default_values(scale):
    scale.set_update_policy(gtk.UPDATE_CONTINUOUS)
    scale.set_digits(1)
    scale.set_value_pos(gtk.POS_TOP)
    scale.set_draw_value(True)
    
# ----- GRAPH CLASS AGGREGATED TO APP ---- #

class Graph(object):
    def __init__(self):
        self._graphics = None
        self._metrics = None
        self._node_click = Graph.default_callback
        
    def default_callback(self, obj, button):
        pass
    
    def set_callback(self, node_click = None):
        self._node_click = node_click
        
    def create_metrics(self, edge_seq, **kwd):
        self._metrics = GraphMetrics(edge_seq, **kwd)
        
    def show(self, canvas):
        self._graphics = GraphicStuff(canvas, self._metrics, partial(self._node_click, self))
        
    def finalize(self):
        pass
    
    def __getitem__(self, obj):
        return self._metrics.tableau[obj]
    
    def __iter__(self):
        return iter(self._metrics.tableau)
        
# ----- GRAPH APPLICATION WITH GRAPH DRAWING METHODS ----- #

class App(BaseApp):
    
    def __init__(self):
        BaseApp.__init__(self)
        self._graph = None
        
    def create_graph(self, edge_seq, node_click = None):
        if self._graph is not None:
            self._graph.finalize()
        self._graph = Graph()
        self._graph.create_metrics(edge_seq, ratio = 0.3)
        self._graph.set_callback(node_click = node_click)
        
    def show_graph(self):
        self.graph.show(self.canvas)

    @property
    def graph(self):
        if self._graph is None:
            raise AttributeError, "No graph has been created for this App"
        else:
            return self._graph
        
    def zoom_changed(self, adjustment):
        self._graph._graphics.graph_group.set_simple_transform(0.0, 0.0, adjustment.value, 0.0)

        
class GraphicStuff(object):
    DEFAULT_FONT_DESC = pango.FontDescription("times roman 14px")
    DEFAULT_PATTERN = cairo.SolidPattern(1.0, 0.8, 0.5, 1.0)

    def __init__(self, canvas, metrics, node_click):
        self.create_graph_group(canvas)
        self.items = list()
        self.texts = list()
        self.item_to_name = dict()
        self.name_to_obj = metrics.name_to_obj
        self.create_curve_items(metrics.curves)
        self.create_rect_items(metrics.rects)
        for item in self.items:
            self.graph_group.add_child(item)
            if item in self.item_to_name:
                self.setup_item_signals(item, node_click)
        for text in self.texts:
            self.graph_group.add_child(text)
            self.setup_item_signals(text, node_click)

        self.graph_group.set_simple_transform(0.0, 0.0, 1.0, 0.0)
        
    def create_graph_group(self, canvas):
        self.graph_group = goocanvas.Group()
        root = canvas.get_root_item()
        root.add_child(self.graph_group)
        
    def create_curve_items(self, curves):
        for coords in curves:
            L = list("{0:.2f},{1:.2f}".format(x, y) for x, y in coords)
            L[0] = "M" + L[0]
            L[1] = "C" + L[1]
            item = goocanvas.Path(data = " ".join(L), stroke_color = "black")
            self.items.append(item)
            
    def create_rect_items(self, rects):
        for x, y, w, h, label, name in rects:
            #item = goocanvas.Rect(
            #    x=x-0.5*w, y=y-0.5*h, width=w, height=h, stroke_color="black",
            #    fill_color="yellow")
            item = goocanvas.Image(x=x-0.5*w, y=y-0.5*h, width=w, height=h, pattern=self.DEFAULT_PATTERN)
            self.items.append(item)
            self.item_to_name[item] = name
            text = goocanvas.Text(
                x = x, y = y, text = label, anchor=gtk.ANCHOR_CENTER)
            text.set_property("font-desc", self.DEFAULT_FONT_DESC)
            self.texts.append(text)
            self.item_to_name[text] = name
            
    def setup_item_signals(self, item, node_click):
        item.connect ("button_press_event", partial(self.button_press, node_click))
        
    def button_press(self, callback, item, target, event):
        #print "button pressed", item, target, event
        name = self.item_to_name[item]
        obj = self.name_to_obj[name]
        callback(obj, event.button)
            
# ----- EXTRACTION OF GRAPH COORDINATES ----- #

class NodeData(object):
    pass

class DotCode(str):
    DPI = 72
    
    def __new__(cls, *args, **kwd):
        return str.__new__(cls, *args, **kwd)
    
    def extract_curves_and_rects(self):
        pgraph = pydot.graph_from_dot_data(self)
        curves = list()
        for edge in pgraph.get_edges():
            bytes = edge.get_attributes()["pos"]
            pairs = [x.split(",") for x in self.parse_edge_path(bytes).split()]
            coords = [ tuple(float(z) for z in p) for p in pairs ]
            curves.append(coords)
        rects = list()
        for node in pgraph.get_nodes():
            pos = node.get_pos()
            if pos is None:
                continue
            box = [ float(x) for x in pos.strip('"').split(",")]
            box.extend( (float(x.strip('"')) * self.DPI
                for x in (node.get_width(), node.get_height())) )
            box.append(node.get_label())
            box.append(node.get_name())
            rects.append(tuple(box))
        return curves, rects
    
    def parse_edge_path(self, bytes):
        return bytes.strip('"').strip().replace("\\\n", "")
    
class GraphMetrics(object):
    def __init__(self, edge_seq, **kwd):
        self.create_tableau(edge_seq)
        self.fill_node_data()
        self.dot_kwd = kwd
        self._curves, self._rects = None, None
        self.name_to_obj = dict(((node.name, obj) for obj, node in self.tableau.items()))

    @property
    def curves(self):
        if self._curves is None:
            self.compute()
        return self._curves
    
    @property
    def rects(self):
        if self._rects is None:
            self.compute()
        return self._rects
    
    def compute(self):
        dot_code = self.create_dot_code(**self.dot_kwd)
        self._curves, self._rects = dot_code.extract_curves_and_rects()

    def create_tableau(self, edge_seq):
        self.tableau = defaultdict(NodeData)
        self.pairs = set()
        for edge in edge_seq:
            assert isinstance(edge, tuple)
            for x in edge:
                self.tableau[x]
            for x in edge[1:]:
                self.pairs.add((edge[0], x))
        self.tableau = dict(self.tableau)

    def fill_node_data(self):
        for i, (x, node) in enumerate(self.tableau.iteritems()):
            node.name = "n{0}".format(i)
            node.label = "<{0} id={1}>".format(x.__class__.__name__, id(x))

    def create_dot_code(self, **kwd):
        agraph = pygraphviz.AGraph(**kwd)
        for node in self.tableau.values():
            agraph.add_node(node.name, label=node.label, shape="box")
        for x, y in self.pairs:
            agraph.add_edge(*(self.tableau[z].name for z in (x, y)))
        agraph.layout("circo")
        dot_code = DotCode(agraph.to_string())
        return dot_code

# ----- TEST CODE ----- #

def gen_example_graph():
    for x in  "ab ac bc cd de df ef".strip().split():
        yield tuple(x)
        
if __name__ == "__main__":

    def on_click(graph, obj, button):
        # this will be called when the user clicks on a graph node
        print "{0} was clicked with button {1}".format(repr(obj), button)

    def pairs():
        # example graph: take a series of words and connect those with at least 3 common letters
        words = "format frame framework function game generator grid gui help".split()
        for i, w0 in enumerate(words):
            for w1 in words[i+1:]:
                if sum(x in w1 for x in set(w0)) >= 3:
                    yield w0, w1
    app = App()
    app.create_graph(pairs(), node_click = on_click)
    for obj in app.graph:
        app.graph[obj].label = str(obj).capitalize()
    app.show_graph()
    app.main()

    
